/*
  This file is part of JCM.

  JCM is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  JCM is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with JCM.  If not, see <http://www.gnu.org/licenses/>.
*/
package neo.bcb;

import java.io.*;
import java.math.BigDecimal;
import java.net.*;
import java.util.*;

import javax.xml.stream.*;

import org.apache.axis.AxisFault;

/**
 * Classe intermediadora entre o web service Sistema Gerenciador de Séries Temporais
 * do Banco Central do Brasil (aka SGS do BCB) e aplicativos clientes, extraindo dados
 * das séries temporais requisitadas.
*/
public class Aurea
{
  /** Stub do webservice. */
  public FachadaWSSGSSoapBindingStub stub;

  /** Client side do webservice. */
  private FachadaWSSGSServiceLocator service;

  /** Pointer para único factory de StAX Parsers. */
  private XMLInputFactory inputFactory;

  /** Construtor de objetos desta classe. */
  protected Aurea()
  {
    // instancia cliente do webservice
    service = new FachadaWSSGSServiceLocator();

    // opcionalmente obtêm "time out" do web service
    // declarado nas propriedades do sistema
    String tm = System.getProperty("webservice.timeout");
    int timeout = (tm == null) ? 0 : Integer.parseInt(tm);

    // instancia stub do webservice
    try {
      stub = new FachadaWSSGSSoapBindingStub(
        new URL(service.getFachadaWSSGSAddress()), service);

      if (timeout > 0) stub.setTimeout(timeout);

    } catch (MalformedURLException e) {
      System.err.println(e);
    } catch (AxisFault e) {
      System.err.println(e);
    }

    // instancia factory de StAX Parsers
    try {
      inputFactory = XMLInputFactory.newInstance();
    } catch (FactoryConfigurationError e) {
      System.err.println(e);
    }
  }

  /** Pointer para instância única de objeto desta classe. */
  private static Aurea aurea;

  /**
   * Acessa instância única de objeto desta classe.
   *
   * @return Instância única de objeto desta classe.
  */
  public static Aurea getInstance()
  {
    return (aurea == null) ? aurea = new Aurea() : aurea;
  }

  /** Representação de componente de séries temporais. */
  public class VO
  {
    /** Data da observação. */
    protected Date date;

    /** Valor numérico de alta precisão para o mês desta data. */
    protected BigDecimal value;

    /** Construtor default de objeto desta classe. */
    @SuppressWarnings("deprecation")
    public VO()
    {
      // inicializa data no instante zero
      date = new Date(0, 0, 0, 0, 0, 0);
    }

    /**
     * Construtor de objeto desta classe com inicialização num instante específico.
     *
     * @param ano Ano da data da observação.
     * @param mes Mês da data da observação.
     * @param dia Dia da data da observação.
    */
    @SuppressWarnings("deprecation")
    public VO(int ano, int mes, int dia)
    {
      // inicializa data no instante específico
      date = new Date(ano, mes, dia, 0, 0, 0);
    }

    /**
     * Acessa a data da observação.
     *
     * @return Date contendo a data da observação.
    */
    public Date getDate() { return date; }

    /**
     * Acessa o valor numérico para o mês desta data.
     *
     * @return valor Bigdecimal correspondente à data observada.
    */
    public BigDecimal getValue() { return value; }

    /**
     * Assinala o valor numérico para o mês desta data.
     *
     * @param s Representação <b>String</b> do valor.
    */
    protected void setValue(String s)
    {
      value = new BigDecimal(s.replaceAll(",", "."));
    }
  }

  /** ExcludeFilter p/uso exclusivo no getter que requisita a última observação... */
  private ExcludeFilter filter1;

  /**
   * Requisita a última observação disponível de uma série temporal.
   *
   * @param code Representação String do código da série temporal no SGS do BCB.
   * @return Observação Aurea.VO singular da série temporal.
   * @throws Exception Qualquer excessão do Axis ou do XMLStreamReader.
  */
  @SuppressWarnings("deprecation")
  public VO get(String code) throws Exception
  {
    VO vo = null;

    try {

      // requisita a última observação disponível da série temporal
      String xmlResponse = stub.getUltimoValorXML( Long.parseLong(code) );

      // cria o parser "ordinário" do documento XML resposta
      XMLStreamReader rd = inputFactory.createXMLStreamReader(
        new StringReader(xmlResponse));

      // instancia o filtro de exclusão de elementos p/facilitar extração de dados
      if (filter1 == null) filter1 = new ExcludeFilter(
        new String[] {"NOME", "CODIGO", "PERIODICIDADE", "UNIDADE"});

      // cria um novo parser combinando o fitro de exclusão com parser "ordinário"
      XMLStreamReader reader = inputFactory.createFilteredReader(rd, filter1);

      // instancia o container dos dados requisitados
      vo =  new VO();

      // loop de pesquisa da tag inicial do único elemento DIA
      while (reader.hasNext())
        if ((reader.next() == XMLStreamConstants.START_ELEMENT)
            && reader.getLocalName().equals("DIA")) break;

      // extrai o número do dia no mês
      int number = Integer.parseInt( reader.getElementText() );
      vo.date.setDate(number);

      // avança até a tag inicial do elemento MES
      reader.nextTag();
      // extrai o número do mês (JAN=0, FEV=1, ..., DEZ=11)
      number = Integer.parseInt( reader.getElementText() );
      vo.date.setMonth(number - 1);

      // avança até a tag inicial do elemento ANO
      reader.nextTag();
      // extrai o número do ano :: calcula o offset p/ano base 1900
      number = Integer.parseInt( reader.getElementText() );
      vo.date.setYear(number - 1900);

      // loop até a tag inicial do elemento VALOR
      do reader.nextTag(); while ( !reader.isStartElement() );
      // extrai o valor da variação percentual na data da observação
      vo.setValue( reader.getElementText() );

      reader.close(); // libera resources utilizados

    } catch(Exception e) { // retransmite qualquer excessão
      throw e;
    }

    return vo;
  }

  /** ExcludeFilter p/uso exclusivo no getter que requisita sub-série temporal... */
  private ExcludeFilter filter2;

  /**
   * Requisita uma sub-série temporal num intervalo.
   *
   * @param code Representação String do código da série temporal no SGS do BCB.
   * @param dataInicial Representação String da data inicial da sub-série.
   * @param dataFinal Representação String da data final da sub-série.
   * @return Array de objetos componentes da sub-série.
   * @throws Exception Qualquer excessão do Axis ou do parser SAX2.
  */
  @SuppressWarnings("deprecation")
  public Object[] get(String code, String dataInicial, String dataFinal) throws Exception
  {
    // container dinâmico da série de observações
    List<VO> vector = new ArrayList<VO>();

    try {

      // requisita sub-série temporal de observações num intervalo
      String xmlResponse = stub.getValoresSeriesXML(
        new long[] { Long.parseLong(code) }, dataInicial, dataFinal);

      // cria o parser "ordinário" do documento XML resposta
      XMLStreamReader rd = inputFactory.createXMLStreamReader(
        new StringReader(xmlResponse));

      // instancia o filtro de exclusão de elementos p/facilitar extração de dados
      if (filter2 == null) filter2 = new ExcludeFilter(new String[] {"BLOQUEADO"});

      // cria um novo parser combinando o fitro de exclusão com parser "ordinário"
      XMLStreamReader reader = inputFactory.createFilteredReader(rd, filter2);

      // loop pesquisando todos os elementos DATA
      while (reader.hasNext())
        if ((reader.next() == XMLStreamConstants.START_ELEMENT)
            && reader.getLocalName().equals("DATA"))
        {
          // particiona string contendo a data da observação
          String[] fields = reader.getElementText().split("/");
          int j = fields.length - 1;
          // instancia o componente de observação singular da série
          VO vo = new VO(
            Integer.parseInt(fields[j]) - 1900,               // ano
            (--j < 0) ? 0 : Integer.parseInt(fields[j]) - 1,  // mês
            (--j < 0) ? 1 : Integer.parseInt(fields[j]));     // dia
          // avança até a tag inicial do elemento VALOR
          reader.nextTag();
          // assinala o valor numérico correspondente à data
          vo.setValue( reader.getElementText() );
          // adiciona a observação ao final do container
          vector.add(vo);
        }

      reader.close(); // libera resources utilizados

    } catch (Exception e) { // retransmite qualquer excessão
      throw e;
    }

    return vector.toArray();
  }

  /**
   * Consulta via linha de comando.
   *
   * @param args
  */
  public static void main(String[] args)
  {
    if (!(args.length == 3 && args[0].matches("\\d+")
          && args[1].matches("\\d+/\\d+/\\d{4}")
          && args[2].matches("\\d+/\\d+/\\d{4}"))
         && !(args.length == 1 && args[0].matches("\\d+")))
    {
      System.out.format("%nUso: java -jar bcb.jar Codigo [dataInicial dataFinal]%n%nCodigo é o código numérico da série temporal.%nDatas no formato dd/mm/aaaa.%n%n");
      System.exit(1);
    }
    String code = args[0], di = null, df = null;
    if (args.length == 3) {
      di = args[1];
      df = args[2];
    }

    try {

      Aurea aurea = Aurea.getInstance();

      System.out.format("SERVICE TIME OUT = %d%n%n", aurea.stub.getTimeout());

      WSSerieVO wo = aurea.stub.getUltimoValorVO(Long.parseLong(code));

      System.out.format("Código: %d%nNome Abreviado: %s%nNome Completo: %s%nGestor: %s%nFonte: %s%nAviso: %s%nPublica: %B%nPeriodicidade: %s%nSigla da Periodicidade: %s%nUnidade Padrão: %s%nData 1ª Observação: %d/%d/%d%nData Última Observação: %d/%d/%d%n%n",
        wo.getOid(),
        wo.getNomeAbreviado(), wo.getNomeCompleto(),
        wo.getGestorProprietario(), wo.getFonte(),
        wo.getAviso(), wo.isPublica(),
        wo.getPeriodicidade(), wo.getPeriodicidadeSigla(),
        wo.getUnidadePadrao(),
        wo.getDiaInicio(), wo.getMesInicio(), wo.getAnoInicio(),
        wo.getDiaFim(), wo.getMesFim(), wo.getAnoFim()
        );

      if (wo.getValores() != null)
      {
        System.out.print("[");
        for (WSValorSerieVO valor: wo.getValores())
        {
          System.out.format(" %s", valor.getSvalor());
        }
        System.out.format(" ]%n%n");
      }

      // checa se todos os argumentos foram fornecidos
      if (args.length == 3)
      {
        Aurea.VO vo = aurea.get(code);

        System.out.format("%1$td/%1$tb/%1$tY %2$ 6.4f%n%n", vo.getDate(), vo.getValue());

        Object[] array = aurea.get(code, di, df);
        for (Object object: array)
        {
          vo = (Aurea.VO) object;
          System.out.format("%1$td/%1$tb/%1$tY %2$ 6.4f%n", vo.getDate(), vo.getValue());
        }
      }

    } catch (Exception e) {
      if (e instanceof AxisFault) {
        String fault = ((AxisFault) e).getFaultString();
        fault = (fault.length() == 0) ? "Excessão Remota desconhecida." : fault.substring(fault.indexOf(":")+2);
        System.out.format("Falha via Axis: %s%n", fault);
      } else {
        e.printStackTrace();
      }
    }
  }

  /** Filtro por exclusão de elementos para StAX Parser. */
  private class ExcludeFilter implements StreamFilter
  {
    /** Array ordenado em ordem crescente dos nomes de elementos a excluir. */
    private String[] tagNames;

    /**
     * Construtor da classe visando performance otimizada.
     *
     * @param tagNames Array contendo os nomes de elementos a excluir.
    */
    public ExcludeFilter(String[] tagNames) { Arrays.sort(this.tagNames = tagNames); }

    /**
     * Verifica exclusão do elemento sob o cursor no StAX Parser.
     *
     * @param rd StAX Parser de documento XML sujeito a exclusão de elementos.
     * @return Status de exclusão do elemento sob o cursor.
    */
    public boolean accept(XMLStreamReader rd)
    {
      return !rd.hasName() || (Arrays.binarySearch(tagNames, rd.getLocalName()) < 0);
    }
  }
}